package core

import (
	"fmt"
	"net/http"

	"github.com/goccy/go-json"
	"github.com/tidwall/gjson"

	"codeberg.org/vnpower/pixivfe/v2/audit"
	"codeberg.org/vnpower/pixivfe/v2/i18n"
	"codeberg.org/vnpower/pixivfe/v2/server/session"
)

// Pixivision represents a Pixivision article as returned by the landing page endpoint.
//
// Note that this type is less complete than the PixivisionArticle type.
type Pixivision struct {
	ID        string `json:"id"`
	Title     string `json:"title"`
	Thumbnail string `json:"thumbnailUrl"`
	URL       string `json:"url"`
}

// RecommendedTags groups artworks under a specific tag recommendation.
type RecommendedTags struct {
	Name     string `json:"tag"`
	Artworks []ArtworkBrief
}

// LandingArtworks aggregates various categories of artworks and other related data
// to be displayed on the landing page.
type LandingArtworks struct {
	Commissions     []ArtworkBrief
	Following       []ArtworkBrief
	Recommended     []ArtworkBrief
	Newest          []ArtworkBrief
	Rankings        Ranking
	Users           []ArtworkBrief
	Pixivision      []Pixivision
	RecommendByTags []RecommendedTags
}

type Pages struct {
	Pixivision       []Pixivision       `json:"pixivision"`
	Follow           []int              `json:"follow"`
	Recommended      RecommendedSection `json:"recommend"`
	RecommendedByTag []RecommendByTag   `json:"recommendByTag"`
	// Commented out fields that aren't currently implemented in the frontend
	// EditorRecommended []any `json:"editorRecommend"`
	// UserRecommended   []any `json:"recommendUser"`
	// Commission        []any `json:"completeRequestIds"`
}

type RecommendedSection struct {
	IDs []string `json:"ids"`
}

type RecommendByTag struct {
	Name string   `json:"tag"`
	IDs  []string `json:"ids"`
}

// Constants for calling GetNewestFromFollowing when len(followIDs) == 0
const (
	// FollowingMode = "all"
	FollowingPage = "1"
)

// landingToRankingMode maps landing page modes to their corresponding ranking modes.
var landingToRankingMode = map[string]string{
	"all": "daily",
	"r18": "daily_r18",
}

// GetLanding retrieves and organizes the landing page data based on the provided mode and user login status.
//
// It fetches raw data from the landing URL, parses the JSON response, and populates the LandingArtworks struct.
func GetLanding(auditor *audit.Auditor, r *http.Request, mode string, isLoggedIn bool) (*LandingArtworks, error) {
	ctx := &RequestContext{auditor: auditor, request: r}
	URL := GetLandingURL(mode)

	resp, err := API_GET_UnwrapJson(ctx.request.Context(), ctx.auditor, URL, "", ctx.request.Header)
	if err != nil {
		return nil, err
	}

	resp, err = session.ProxyImageUrl(ctx.request, resp)
	if err != nil {
		return nil, err
	}

	if !gjson.Valid(resp) {
		return nil, i18n.Errorf("Invalid JSON: %v", resp)
	}

	artworks, err := parseArtworks(resp)
	if err != nil {
		return nil, err
	}

	// Populate thumbnails for each artwork
	for id, artwork := range artworks {
		if err := artwork.PopulateThumbnails(auditor); err != nil {
			auditor.SugaredLogger.Errorf("Failed to populate thumbnails for artwork ID %s: %v", id, err)
			return nil, fmt.Errorf("failed to populate thumbnails for artwork ID %s: %w", id, err)
		}
		artworks[id] = artwork
	}

	var pages Pages
	pagesStr := gjson.Get(resp, "page").String()

	if err := json.Unmarshal([]byte(pagesStr), &pages); err != nil {
		return nil, err
	}

	landing := LandingArtworks{
		Pixivision: pages.Pixivision,
	}

	// If the user is logged in, populate personalized sections
	if isLoggedIn {
		landing.Following, err = ctx.populateFollowing(mode, pages.Follow, artworks)
		if err != nil {
			return nil, err
		}

		landing.RecommendByTags = populateRecommendedByTags(pages.RecommendedByTag, artworks)
		landing.Recommended = populateArtworks(pages.Recommended.IDs, artworks)
	}

	landing.Rankings, err = ctx.fetchRankings(mode)
	if err != nil {
		return nil, err
	}

	return &landing, nil
}

// parseArtworks extracts artwork information from the "thumbnails.illust" field
// of the JSON response and maps them by their ID.
func parseArtworks(resp string) (map[string]ArtworkBrief, error) {
	artworks := make(map[string]ArtworkBrief)

	stuff := gjson.Get(resp, "thumbnails.illust")

	// Iterate over each thumbnail entry and unmarshal it into the ArtworkBrief struct
	stuff.ForEach(func(key, value gjson.Result) bool {
		var artwork ArtworkBrief
		if err := json.Unmarshal([]byte(value.String()), &artwork); err != nil {
			// Invalid artwork entries are simply skipped
			return false
		}

		// Map the artwork by its ID for easy access later
		if artwork.ID != "" {
			artworks[artwork.ID] = artwork
		}

		return true // Continue iteration
	})

	return artworks, nil
}

// populateArtworks is a generic helper function that maps a slice of string IDs
// to their corresponding ArtworkBrief objects.
func populateArtworks(ids []string, artworks map[string]ArtworkBrief) []ArtworkBrief {
	populated := make([]ArtworkBrief, 0, len(ids))

	for _, id := range ids {
		if artwork, exists := artworks[id]; exists {
			populated = append(populated, artwork)
		}
	}

	return populated
}

// populateFollowing converts int IDs to strings and uses the generic helper.
//
// If followIDs is empty, it attempts population by calling GetNewestFromFollowing instead (pixiv moment).
func (ctx *RequestContext) populateFollowing(mode string, followIDs []int, artworks map[string]ArtworkBrief) ([]ArtworkBrief, error) {
	if len(followIDs) == 0 {
		return GetNewestFromFollowing(ctx.auditor, ctx.request, mode, FollowingPage)
	}

	stringIDs := make([]string, len(followIDs))
	for i, id := range followIDs {
		stringIDs[i] = fmt.Sprint(id)
	}

	return populateArtworks(stringIDs, artworks), nil
}

// populateRecommended uses the generic helper.
func populateRecommended(recommendedIDs []string, artworks map[string]ArtworkBrief) []ArtworkBrief {
	return populateArtworks(recommendedIDs, artworks)
}

// populateRecommendedByTags uses the generic helper for each tag's IDs.
func populateRecommendedByTags(recommends []RecommendByTag, artworks map[string]ArtworkBrief) []RecommendedTags {
	recommendByTags := make([]RecommendedTags, 0, len(recommends))

	for _, r := range recommends {
		artworksList := populateArtworks(r.IDs, artworks)
		recommendByTags = append(recommendByTags, RecommendedTags{
			Name:     r.Name,
			Artworks: artworksList,
		})
	}

	return recommendByTags
}

// fetchRankings retrieves the current rankings based on the selected mode.
//
// It maps the landing page mode to the appropriate ranking mode and fetches the ranking data.
func (ctx *RequestContext) fetchRankings(mode string) (Ranking, error) {
	rankingMode, exists := landingToRankingMode[mode]
	if !exists {
		rankingMode = "daily"
	}
	return GetRanking(ctx.auditor, ctx.request, rankingMode, "illust", "", "1")
}
